/*
 * Copyright (c) 2010-2022 Belledonne Communications SARL.
 *
 * This file is part of mediastreamer2
 * (see https://gitlab.linphone.org/BC/public/mediastreamer2).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <bctoolbox/defs.h>

#include "mediastreamer2/android_utils.h"
#include "mediastreamer2/devices.h"
#include "mediastreamer2/msfilter.h"
#include "mediastreamer2/msjava.h"
#include "mediastreamer2/mssndcard.h"
#include "mediastreamer2/msticker.h"
#include <jni.h>

#include <sys/resource.h>
#include <sys/time.h>

#define USE_HARDWARE_RATE 1

static const float sndwrite_flush_threshold = 0.020; // ms
static const float sndread_flush_threshold = 0.020;  // ms
static void sound_read_setup(MSFilter *f);

static void set_high_prio(void) {
	/*
	    This pthread based code does nothing on linux. The linux kernel has
	    sched_get_priority_max(SCHED_OTHER)=sched_get_priority_max(SCHED_OTHER)=0.
	    As long as we can't use SCHED_RR or SCHED_FIFO, the only way to increase priority of a calling thread
	    is to use setpriority().
	*/
#if 0
	struct sched_param param;
	int result=0;
	memset(&param,0,sizeof(param));
	int policy=SCHED_OTHER;
	param.sched_priority=sched_get_priority_max(policy);
	if((result=pthread_setschedparam(pthread_self(),policy, &param))) {
		ms_warning("Set sched param failed with error code(%i)\n",result);
	} else {
		ms_message("msandroid thread priority set to max (%i, min=%i)",sched_get_priority_max(policy),sched_get_priority_min(policy));
	}
#endif
	if (setpriority(PRIO_PROCESS, 0, -20) == -1) {
		ms_warning("msandroid set_high_prio() failed: %s", strerror(errno));
	}
}
/*
 mediastreamer2 sound card functions
 */

void msandroid_sound_set_level(BCTBX_UNUSED(MSSndCard *card),
                               BCTBX_UNUSED(MSSndCardMixerElem e),
                               BCTBX_UNUSED(int percent)) {
}

int msandroid_sound_get_level(BCTBX_UNUSED(MSSndCard *card), BCTBX_UNUSED(MSSndCardMixerElem e)) {
	return 0;
}

void msandroid_sound_set_source(BCTBX_UNUSED(MSSndCard *card), BCTBX_UNUSED(MSSndCardCapture source)) {
}

static int sdk_version = 0;

void msandroid_sound_init(BCTBX_UNUSED(MSSndCard *card)) {
	/*get running sdk version*/
	JNIEnv *jni_env = ms_get_jni_env();
	jclass version_class = jni_env->FindClass("android/os/Build$VERSION");
	jfieldID fid = jni_env->GetStaticFieldID(version_class, "SDK_INT", "I");
	sdk_version = jni_env->GetStaticIntField(version_class, fid);
	ms_message("SDK version [%i] detected", sdk_version);
	jni_env->DeleteLocalRef(version_class);
}

void msandroid_sound_uninit(BCTBX_UNUSED(MSSndCard *card)) {
}

void msandroid_sound_detect(MSSndCardManager *m);
MSSndCard *msandroid_sound_duplicate(MSSndCard *obj);

MSFilter *msandroid_sound_read_new(MSSndCard *card);
MSFilter *msandroid_sound_write_new(MSSndCard *card);

MSSndCardDesc msandroid_sound_card_desc = {
    /*.driver_type=*/"ANDROID SND (deprecated)",
    /*.detect=*/msandroid_sound_detect,
    /*.init=*/msandroid_sound_init,
    /*.set_level=*/msandroid_sound_set_level,
    /*.get_level=*/msandroid_sound_get_level,
    /*.set_capture=*/msandroid_sound_set_source,
    /*.set_control=*/NULL,
    /*.get_control=*/NULL,
    /*.create_reader=*/msandroid_sound_read_new,
    /*.create_writer=*/msandroid_sound_write_new,
    /*.uninit=*/msandroid_sound_uninit,
    /*.duplicate=*/msandroid_sound_duplicate};

MSSndCard *msandroid_sound_duplicate(MSSndCard *obj) {
	MSSndCard *card = ms_snd_card_new(&msandroid_sound_card_desc);
	card->name = ms_strdup(obj->name);
	card->device_description = obj->device_description;
	return card;
}

MSSndCard *msandroid_sound_card_new(MSSndCardManager *m) {
	SoundDeviceDescription *d = NULL;
	MSDevicesInfo *devices = NULL;
	MSSndCard *card = ms_snd_card_new(&msandroid_sound_card_desc);
	card->name = ms_strdup("Android Sound card");

	devices = ms_factory_get_devices_info(m->factory);
	d = ms_devices_info_get_sound_device_description(devices);
	card->device_description = d;

	if (d->flags & DEVICE_HAS_BUILTIN_AEC) {
		card->capabilities |= MS_SND_CARD_CAP_BUILTIN_ECHO_CANCELLER;
	}
	card->data = d;
	return card;
}

void msandroid_sound_detect(MSSndCardManager *m) {
	ms_debug("msandroid_sound_detect");
	MSSndCard *card = msandroid_sound_card_new(m);
	ms_snd_card_manager_add_card(m, card);
}

/*************filter commun functions*********/
class msandroid_sound_data {
public:
	msandroid_sound_data() : bits(16), rate(8000), nchannels(1), started(false), thread_id(0), forced_rate(false) {
		ms_mutex_init(&mutex, NULL);
	};
	~msandroid_sound_data() {
		ms_mutex_destroy(&mutex);
	}
	unsigned int bits;
	unsigned int rate;
	unsigned int nchannels;
	bool started;
	ms_thread_t thread_id;
	ms_mutex_t mutex;
	int buff_size; /*buffer size in bytes*/
	bool forced_rate;
};

static int get_rate(MSFilter *f, void *data) {
	msandroid_sound_data *d = (msandroid_sound_data *)f->data;
	*(int *)data = d->rate;
	return 0;
}

static int set_nchannels(MSFilter *f, void *arg) {
	ms_debug("set_nchannels %d", *((int *)arg));
	msandroid_sound_data *d = (msandroid_sound_data *)f->data;
	d->nchannels = *(int *)arg;
	return 0;
}

static int get_nchannels(MSFilter *f, void *arg) {
	ms_debug("get_nchannels %d", *((int *)arg));
	msandroid_sound_data *d = (msandroid_sound_data *)f->data;
	*(int *)arg = d->nchannels;
	return 0;
}

static unsigned int get_supported_rate(unsigned int prefered_rate) {
	JNIEnv *jni_env = ms_get_jni_env();
	jclass audio_record_class = jni_env->FindClass("android/media/AudioRecord");
	int size = jni_env->CallStaticIntMethod(
	    audio_record_class, jni_env->GetStaticMethodID(audio_record_class, "getMinBufferSize", "(III)I"), prefered_rate,
	    2 /*CHANNEL_CONFIGURATION_MONO*/
	    ,
	    2 /*  ENCODING_PCM_16BIT */);

	if (size > 0) {
		return prefered_rate;
	} else {
		ms_warning("Cannot configure recorder with rate [%i]", prefered_rate);
		if (prefered_rate > 48000) {
			return get_supported_rate(48000);
		}
		switch (prefered_rate) {
			case 12000:
			case 24000:
				return get_supported_rate(48000);
			case 48000:
				return get_supported_rate(44100);
			case 44100:
				return get_supported_rate(16000);
			case 16000:
				return get_supported_rate(8000);
			default: {
				ms_error("This Android sound card doesn't support any standard sample rate");
				return 0;
			}
		}

		return 0;
	}
}

/***********************************read filter********************/
static int set_read_rate(MSFilter *f, void *arg) {
	unsigned int proposed_rate = *((unsigned int *)arg);
	ms_debug("set_rate %d", proposed_rate);
	msandroid_sound_data *d = (msandroid_sound_data *)f->data;
	if (d->forced_rate) {
		ms_warning("sample rate is forced by mediastreamer2 device table, skipping...");
		return -1;
	}

	d->rate = get_supported_rate(proposed_rate);
	if (d->rate == proposed_rate) return 0;
	else return -1;
}

static int get_latency(MSFilter *f, void *arg) {
	msandroid_sound_data *d = (msandroid_sound_data *)f->data;
	if (!d->started) {
		sound_read_setup(f);
		*((int *)arg) = (1000 * d->buff_size) / (d->nchannels * 2 * d->rate);
	}
	return 0;
}

static int msandroid_hack_speaker_state(MSFilter *f, void *arg);

MSFilterMethod msandroid_sound_read_methods[] = {{MS_FILTER_SET_SAMPLE_RATE, set_read_rate},
                                                 {MS_FILTER_GET_SAMPLE_RATE, get_rate},
                                                 {MS_FILTER_SET_NCHANNELS, set_nchannels},
                                                 {MS_FILTER_GET_NCHANNELS, get_nchannels},
                                                 {MS_FILTER_GET_LATENCY, get_latency},
                                                 {MS_AUDIO_CAPTURE_FORCE_SPEAKER_STATE, msandroid_hack_speaker_state},
                                                 {0, NULL}};

class msandroid_sound_read_data : public msandroid_sound_data {
public:
	msandroid_sound_read_data() : audio_record(0), audio_record_class(0), read_buff(0), read_chunk_size(0) {
		ms_bufferizer_init(&rb);
		aec = NULL;
	}
	~msandroid_sound_read_data() {
		ms_bufferizer_uninit(&rb);
	}
	jobject audio_record;
	jclass audio_record_class;
	jbyteArray read_buff;
	MSBufferizer rb;
	int read_chunk_size;
	int framesize;
	int outgran_ms;
	int min_avail;
	int64_t start_time;
	int64_t read_samples;
	MSTickerSynchronizer *ticker_synchronizer;
	jobject aec;
	bool builtin_aec;
};

static void *msandroid_read_cb(msandroid_sound_read_data *d) {
	mblk_t *m;
	int nread;
	jmethodID read_id = 0;
	jmethodID record_id = 0;

	set_high_prio();

	JNIEnv *jni_env = ms_get_jni_env();
	record_id = jni_env->GetMethodID(d->audio_record_class, "startRecording", "()V");
	if (record_id == 0) {
		ms_error("cannot find AudioRecord.startRecording() method");
		goto end;
	}
	// start recording
	jni_env->CallVoidMethod(d->audio_record, record_id);

	// int read (byte[] audioData, int offsetInBytes, int sizeInBytes)
	read_id = jni_env->GetMethodID(d->audio_record_class, "read", "([BII)I");
	if (read_id == 0) {
		ms_error("cannot find AudioRecord.read() method");
		goto end;
	}

	while (d->started &&
	       (nread = jni_env->CallIntMethod(d->audio_record, read_id, d->read_buff, 0, d->read_chunk_size)) > 0) {
		m = allocb(nread, 0);
		jni_env->GetByteArrayRegion(d->read_buff, 0, nread, (jbyte *)m->b_wptr);
		// ms_error("%i octets read",nread);
		m->b_wptr += nread;
		d->read_samples += nread / (2 * d->nchannels);
		ms_ticker_synchronizer_update(d->ticker_synchronizer, d->read_samples, d->rate);
		ms_mutex_lock(&d->mutex);
		ms_bufferizer_put(&d->rb, m);
		ms_mutex_unlock(&d->mutex);
	};

	goto end;
end: {
	ms_thread_exit(NULL);
	return 0;
}
}

static void sound_read_setup(MSFilter *f) {
	ms_debug("andsnd_read_preprocess");
	msandroid_sound_read_data *d = (msandroid_sound_read_data *)f->data;
	jmethodID constructor_id = 0, methodID = 0;
	int audio_record_state = 0;
	jmethodID min_buff_size_id;
	// jmethodID set_notification_period;
	int rc;

	JNIEnv *jni_env = ms_get_jni_env();
	d->audio_record_class = (jclass)jni_env->NewGlobalRef(jni_env->FindClass("android/media/AudioRecord"));
	if (d->audio_record_class == 0) {
		ms_error("cannot find android/media/AudioRecord");
		return;
	}

	constructor_id = jni_env->GetMethodID(d->audio_record_class, "<init>", "(IIIII)V");
	if (constructor_id == 0) {
		ms_error("cannot find AudioRecord (int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, "
		         "int bufferSizeInBytes)");
		return;
	}
	min_buff_size_id = jni_env->GetStaticMethodID(d->audio_record_class, "getMinBufferSize", "(III)I");
	if (min_buff_size_id == 0) {
		ms_error("cannot find AudioRecord.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)");
		return;
	}
	d->buff_size = jni_env->CallStaticIntMethod(d->audio_record_class, min_buff_size_id, d->rate,
	                                            2 /*CHANNEL_CONFIGURATION_MONO*/, 2 /*  ENCODING_PCM_16BIT */);
	d->read_chunk_size = d->buff_size / 4;
	d->buff_size *= 2; /*double the size for configuring the recorder: this does not affect latency but prevents
	                      "AudioRecordThread: buffer overflow"*/

	if (d->buff_size > 0) {
		ms_message("Configuring recorder with [%i] bits  rate [%i] nchanels [%i] buff size [%i], chunk size [%i]",
		           d->bits, d->rate, d->nchannels, d->buff_size, d->read_chunk_size);
	} else {
		ms_message("Cannot configure recorder with [%i] bits  rate [%i] nchanels [%i] buff size [%i] chunk size [%i]",
		           d->bits, d->rate, d->nchannels, d->buff_size, d->read_chunk_size);
		return;
	}

	d->read_buff = jni_env->NewByteArray(d->buff_size);
	d->read_buff = (jbyteArray)jni_env->NewGlobalRef(d->read_buff);
	if (d->read_buff == 0) {
		ms_error("cannot instanciate read buff");
		return;
	}

	d->audio_record = jni_env->NewObject(d->audio_record_class, constructor_id,
	                                     sdk_version < 11 ? 1 /*MIC*/ : 7 /*VOICE_COMMUNICATION*/
	                                     ,
	                                     d->rate, 2 /*CHANNEL_CONFIGURATION_MONO*/
	                                     ,
	                                     2 /*  ENCODING_PCM_16BIT */
	                                     ,
	                                     d->buff_size);

	// Check the state of the AudioRecord (uninitialized = 1
	methodID = jni_env->GetMethodID(d->audio_record_class, "getState", "()I");
	if (methodID == 0) {
		ms_error("cannot find AudioRecord getState() method");
		return;
	}
	audio_record_state = jni_env->CallIntMethod(d->audio_record, methodID);

	if (audio_record_state == 1) {
		d->audio_record = jni_env->NewGlobalRef(d->audio_record);
		if (d->audio_record == 0) {
			ms_error("cannot instantiate AudioRecord");
			return;
		}
	} else {
		d->audio_record = NULL;
		ms_error("AudioRecord is not initialized properly. It may be caused by RECORD_AUDIO permission not granted");
	}

	d->min_avail = -1;
	d->read_samples = 0;
	d->ticker_synchronizer = ms_ticker_synchronizer_new();
	d->outgran_ms = 20;
	d->start_time = -1;
	d->framesize = (d->outgran_ms * d->rate) / 1000;
	d->started = true;
	// start reader thread
	if (d->audio_record) {
		rc = ms_thread_create(&d->thread_id, 0, (void *(*)(void *))msandroid_read_cb, d);
		if (rc) {
			ms_error("cannot create read thread return code  is [%i]", rc);
			d->started = false;
		}
	}
}

static void sound_read_preprocess(MSFilter *f) {
	msandroid_sound_read_data *d = (msandroid_sound_read_data *)f->data;
	ms_debug("andsnd_read_preprocess");
	if (!d->started) sound_read_setup(f);
	ms_ticker_set_synchronizer(f->ticker, d->ticker_synchronizer);

	if (d->builtin_aec && d->audio_record) {
		JNIEnv *env = ms_get_jni_env();
		jmethodID getsession_id = 0;
		int sessionId = -1;
		getsession_id = env->GetMethodID(d->audio_record_class, "getAudioSessionId", "()I");
		if (getsession_id == 0) {
			ms_error("cannot find AudioRecord.getAudioSessionId() method");
			return;
		}
		sessionId = env->CallIntMethod(d->audio_record, getsession_id);
		ms_message("AudioRecord.getAudioSessionId() returned %i", sessionId);
		if (sessionId == -1) {
			return;
		}
		d->aec = ms_android_enable_hardware_echo_canceller(env, sessionId);
	}
}

static void sound_read_postprocess(MSFilter *f) {
	msandroid_sound_read_data *d = (msandroid_sound_read_data *)f->data;
	jmethodID stop_id = 0;
	jmethodID release_id = 0;

	JNIEnv *jni_env = ms_get_jni_env();

	ms_ticker_set_synchronizer(f->ticker, NULL);

	// stop recording
	stop_id = jni_env->GetMethodID(d->audio_record_class, "stop", "()V");
	if (stop_id == 0) {
		ms_error("cannot find AudioRecord.stop() method");
		goto end;
	}

	d->started = false;
	if (d->thread_id != 0) {
		ms_thread_join(d->thread_id, 0);
		d->thread_id = 0;
	}

	if (d->audio_record) {
		jni_env->CallVoidMethod(d->audio_record, stop_id);

		// release recorder
		release_id = jni_env->GetMethodID(d->audio_record_class, "release", "()V");
		if (release_id == 0) {
			ms_error("cannot find AudioRecord.release() method");
			goto end;
		}
		jni_env->CallVoidMethod(d->audio_record, release_id);
	}
	if (d->aec) {
		ms_android_delete_hardware_echo_canceller(jni_env, d->aec);
		d->aec = NULL;
	}
	goto end;
end: {
	if (d->ticker_synchronizer) {
		ms_ticker_synchronizer_destroy(d->ticker_synchronizer);
		d->ticker_synchronizer = NULL;
	}
	d->read_samples = 0;
	if (d->audio_record) jni_env->DeleteGlobalRef(d->audio_record);
	jni_env->DeleteGlobalRef(d->audio_record_class);
	if (d->read_buff) jni_env->DeleteGlobalRef(d->read_buff);
	return;
}
}

static void sound_read_process(MSFilter *f) {
	msandroid_sound_read_data *d = (msandroid_sound_read_data *)f->data;
	int nbytes = d->framesize * d->nchannels * 2;
	int avail;
	bool_t flush = FALSE;
	bool_t can_output = (d->start_time == -1 || ((f->ticker->time - d->start_time) % d->outgran_ms == 0));

	ms_mutex_lock(&d->mutex);
	if (!d->started) {
		ms_mutex_unlock(&d->mutex);
		return;
	}
	avail = ms_bufferizer_get_avail(&d->rb);
	if (f->ticker->time % 5000 == 0) {
		if (d->min_avail >= (sndread_flush_threshold * (float)d->rate * 2.0 * (float)d->nchannels)) {
			int excess_ms = (d->min_avail * 1000) / (d->rate * 2 * d->nchannels);
			ms_warning("Excess of audio samples in capture side bytes=%i (%i ms)", d->min_avail, excess_ms);
			can_output = TRUE;
			flush = TRUE;
		}
		d->min_avail = -1;
	}
	do {
		if (can_output &&
		    (avail >=
		     nbytes *
		         2)) { // bytes*2 is to insure smooth output, we leave at least one packet in the buffer for next time*/
			mblk_t *om = allocb(nbytes, 0);
			ms_bufferizer_read(&d->rb, om->b_wptr, nbytes);
			om->b_wptr += nbytes;
			ms_queue_put(f->outputs[0], om);
			// ms_message("Out time=%llu ",f->ticker->time);
			if (d->start_time == -1) d->start_time = f->ticker->time;
			avail -= nbytes;
		} else break;
	} while (flush);
	ms_mutex_unlock(&d->mutex);
	if (d->min_avail == -1 || avail < d->min_avail) d->min_avail = avail;
}

static MSFilterDesc msandroid_sound_read_desc = {
    /*.id=*/MS_FILTER_PLUGIN_ID,
    /*.name=*/"MSAndSoundRead",
    /*.text=*/N_("Sound capture filter for Android"),
    /*.category=*/MS_FILTER_OTHER,
    /*.enc_fmt*/ NULL,
    /*.ninputs=*/0,
    /*.noutputs=*/1,
    /*.init*/ NULL,
    /*.preprocess=*/sound_read_preprocess,
    /*.process=*/sound_read_process,
    /*.postprocess=*/sound_read_postprocess,
    /*.uninit*/ NULL,
    /*.methods=*/msandroid_sound_read_methods};

MSFilter *msandroid_sound_read_new(MSSndCard *card) {
	ms_debug("msandroid_sound_read_new");
	MSFilter *f = ms_factory_create_filter_from_desc(ms_snd_card_get_factory(card), &msandroid_sound_read_desc);
	msandroid_sound_read_data *data = new msandroid_sound_read_data();
	data->builtin_aec = card->capabilities & MS_SND_CARD_CAP_BUILTIN_ECHO_CANCELLER;
	if (card->data != NULL) {
		SoundDeviceDescription *d = (SoundDeviceDescription *)card->data;
		if (d->recommended_rate > 0) {
			data->rate = d->recommended_rate;
			data->forced_rate = true;
			ms_warning("Using forced sample rate %i", data->rate);
		}
	}
	f->data = data;
	return f;
}

/***********************************write filter********************/
#ifndef _MSC_VER
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#endif // _MSC_VER
static int set_write_rate(MSFilter *f, void *arg) {
#ifndef USE_HARDWARE_RATE
	msandroid_sound_data *d = (msandroid_sound_data *)f->data;
	if (d->forced_rate) {
		ms_warning("sample rate is forced by mediastreamer2 device table, skipping...");
		return -1;
	}

	int proposed_rate = *((int *)arg);
	ms_debug("set_rate %d", proposed_rate);
	d->rate = proposed_rate;
	return 0;
#else
	/*audioflingler resampling is really bad
	we prefer do resampling by ourselves if cpu allows it*/
	return -1;
#endif
}
#ifndef _MSC_VER
#pragma GCC diagnostic pop
#endif // _MSC_VER

MSFilterMethod msandroid_sound_write_methods[] = {{MS_FILTER_SET_SAMPLE_RATE, set_write_rate},
                                                  {MS_FILTER_GET_SAMPLE_RATE, get_rate},
                                                  {MS_FILTER_SET_NCHANNELS, set_nchannels},
                                                  {MS_FILTER_GET_NCHANNELS, get_nchannels},
                                                  {0, NULL}};

class msandroid_sound_write_data : public msandroid_sound_data {
public:
	msandroid_sound_write_data()
	    : audio_track_class(0), audio_track(0), write_chunk_size(0), writtenBytes(0), last_sample_date(0) {
		bufferizer = ms_bufferizer_new();
		ms_cond_init(&cond, 0);
		JNIEnv *jni_env = ms_get_jni_env();
		audio_track_class = (jclass)jni_env->NewGlobalRef(jni_env->FindClass("android/media/AudioTrack"));
		if (audio_track_class == 0) {
			ms_error("cannot find  android/media/AudioTrack\n");
			return;
		}
		jmethodID hwrate_id = jni_env->GetStaticMethodID(audio_track_class, "getNativeOutputSampleRate", "(I)I");
		if (hwrate_id == 0) {
			ms_error("cannot find  int AudioRecord.getNativeOutputSampleRate(int streamType)");
			return;
		}
		rate = jni_env->CallStaticIntMethod(audio_track_class, hwrate_id, 0 /*STREAM_VOICE_CALL*/);
		ms_message("Hardware sample rate is %i", rate);
	};
	~msandroid_sound_write_data() {
		ms_bufferizer_flush(bufferizer);
		ms_bufferizer_destroy(bufferizer);
		ms_cond_destroy(&cond);
		if (audio_track_class != 0) {
			JNIEnv *env = ms_get_jni_env();
			env->DeleteGlobalRef(audio_track_class);
		}
	}
	jclass audio_track_class;
	jobject audio_track;
	MSBufferizer *bufferizer;
	ms_cond_t cond;
	int write_chunk_size;
	unsigned int writtenBytes;
	unsigned long last_sample_date;
	bool sleeping;
	unsigned int getWriteBuffSize() {
		return buff_size;
	}
	int getWrittenFrames() {
		return writtenBytes / (nchannels * (bits / 8));
	}
};

static void *msandroid_write_cb(msandroid_sound_write_data *d) {
	jbyteArray write_buff;
	jmethodID write_id = 0;
	jmethodID play_id = 0;
	int min_size = -1;
	int count;
	int max_size = sndwrite_flush_threshold * (float)d->rate * (float)d->nchannels * 2.0;
	int check_point_size = 3 * (float)d->rate * (float)d->nchannels * 2.0; /*3 seconds*/
	int nwrites = 0;

	int buff_size = d->write_chunk_size;
	uint8_t tmpBuff[buff_size];
	JNIEnv *jni_env = ms_get_jni_env();

	set_high_prio();

	// int write  (byte[] audioData, int offsetInBytes, int sizeInBytes)
	write_id = jni_env->GetMethodID(d->audio_track_class, "write", "([BII)I");
	if (write_id == 0) {
		ms_error("cannot find AudioTrack.write() method");
		goto end;
	}
	play_id = jni_env->GetMethodID(d->audio_track_class, "play", "()V");
	if (play_id == 0) {
		ms_error("cannot find AudioTrack.play() method");
		goto end;
	}
	write_buff = jni_env->NewByteArray(buff_size);

	// start playing
	jni_env->CallVoidMethod(d->audio_track, play_id);

	ms_mutex_lock(&d->mutex);
	ms_bufferizer_flush(d->bufferizer);
	ms_mutex_unlock(&d->mutex);

	while (d->started) {
		int bufferizer_size;

		ms_mutex_lock(&d->mutex);
		min_size = -1;
		count = 0;
		while ((bufferizer_size = ms_bufferizer_get_avail(d->bufferizer)) >= d->write_chunk_size) {
			if (min_size == -1) min_size = bufferizer_size;
			else if (bufferizer_size < min_size) min_size = bufferizer_size;

			ms_bufferizer_read(d->bufferizer, tmpBuff, d->write_chunk_size);
			ms_mutex_unlock(&d->mutex);
			jni_env->SetByteArrayRegion(write_buff, 0, d->write_chunk_size, (jbyte *)tmpBuff);
			int result = jni_env->CallIntMethod(d->audio_track, write_id, write_buff, 0, d->write_chunk_size);
			d->writtenBytes += result;
			if (result <= 0) {
				ms_error("write operation has failed [%i]", result);
			}
			nwrites++;
			ms_mutex_lock(&d->mutex);
			count += d->write_chunk_size;
			if (count > check_point_size) {
				if (min_size > max_size) {
					ms_warning("we are late, flushing %i bytes", min_size);
					ms_bufferizer_skip_bytes(d->bufferizer, min_size);
				}
				count = 0;
			}
		}
		if (d->started) {
			d->sleeping = true;
			ms_cond_wait(&d->cond, &d->mutex);
			d->sleeping = false;
		}
		ms_mutex_unlock(&d->mutex);
	}

	goto end;
end: {
	ms_thread_exit(NULL);
	return NULL;
}
}

void msandroid_sound_write_preprocess(MSFilter *f) {
	ms_debug("andsnd_write_preprocess");
	msandroid_sound_write_data *d = (msandroid_sound_write_data *)f->data;
	jmethodID constructor_id = 0;

	int rc;
	jmethodID min_buff_size_id;

	JNIEnv *jni_env = ms_get_jni_env();

	if (d->audio_track_class == 0) {
		return;
	}

	constructor_id = jni_env->GetMethodID(d->audio_track_class, "<init>", "(IIIIII)V");
	if (constructor_id == 0) {
		ms_error("cannot find  AudioTrack(int streamType, int sampleRateInHz, \
		int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)");
		return;
	}

	min_buff_size_id = jni_env->GetStaticMethodID(d->audio_track_class, "getMinBufferSize", "(III)I");
	if (min_buff_size_id == 0) {
		ms_error("cannot find  AudioTrack.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)");
		return;
	}
	d->buff_size = jni_env->CallStaticIntMethod(d->audio_track_class, min_buff_size_id, d->rate,
	                                            2 /*CHANNEL_CONFIGURATION_MONO*/, 2 /*  ENCODING_PCM_16BIT */);
	d->write_chunk_size = (d->rate * (d->bits / 8) * d->nchannels) * 0.02;
	// d->write_chunk_size=d->buff_size;
	if (d->buff_size > 0) {
		ms_message("Configuring player with [%i] bits  rate [%i] nchanels [%i] buff size [%i] chunk size [%i]", d->bits,
		           d->rate, d->nchannels, d->buff_size, d->write_chunk_size);
	} else {
		ms_message("Cannot configure player with [%i] bits  rate [%i] nchanels [%i] buff size [%i] chunk size [%i]",
		           d->bits, d->rate, d->nchannels, d->buff_size, d->write_chunk_size);
		return;
	}
	d->audio_track = jni_env->NewObject(d->audio_track_class, constructor_id, 0 /*STREAM_VOICE_CALL*/
	                                    ,
	                                    d->rate, 2 /*CHANNEL_CONFIGURATION_MONO*/
	                                    ,
	                                    2 /*  ENCODING_PCM_16BIT */
	                                    ,
	                                    d->buff_size, 1 /*MODE_STREAM */);
	d->audio_track = jni_env->NewGlobalRef(d->audio_track);
	if (d->audio_track == 0) {
		ms_error("cannot instanciate AudioTrack");
		return;
	}

	// start reader thread
	d->started = true;
	rc = ms_thread_create(&d->thread_id, 0, (void *(*)(void *))msandroid_write_cb, d);
	if (rc) {
		ms_error("cannot create write thread return code  is [%i]", rc);
		d->started = false;
		return;
	}
}

void msandroid_sound_write_postprocess(MSFilter *f) {
	msandroid_sound_write_data *d = (msandroid_sound_write_data *)f->data;
	jmethodID flush_id = 0;
	jmethodID stop_id = 0;
	jmethodID release_id = 0;
	JNIEnv *jni_env = ms_get_jni_env();

	d->started = false;
	ms_mutex_lock(&d->mutex);
	ms_cond_signal(&d->cond);
	ms_mutex_unlock(&d->mutex);
	if (d->thread_id != 0) {
		ms_thread_join(d->thread_id, NULL);
		d->thread_id = 0;
	}
	// flush
	flush_id = jni_env->GetMethodID(d->audio_track_class, "flush", "()V");
	if (flush_id == 0) {
		ms_error("cannot find AudioTrack.flush() method");
		goto end;
	}
	if (d->audio_track) {

		jni_env->CallVoidMethod(d->audio_track, flush_id);

		// stop playing
		stop_id = jni_env->GetMethodID(d->audio_track_class, "stop", "()V");
		if (stop_id == 0) {
			ms_error("cannot find AudioTrack.stop() method");
			goto end;
		}
		jni_env->CallVoidMethod(d->audio_track, stop_id);

		// release playing
		release_id = jni_env->GetMethodID(d->audio_track_class, "release", "()V");
		if (release_id == 0) {
			ms_error("cannot find AudioTrack.release() method");
			goto end;
		}
		jni_env->CallVoidMethod(d->audio_track, release_id);
	}

	goto end;
end: {
	if (d->audio_track) jni_env->DeleteGlobalRef(d->audio_track);
	// d->jvm->DetachCurrentThread();
	return;
}
}

void msandroid_sound_write_process(MSFilter *f) {
	msandroid_sound_write_data *d = (msandroid_sound_write_data *)f->data;

	mblk_t *m;
	while ((m = ms_queue_get(f->inputs[0])) != NULL) {
		if (d->started) {
			ms_mutex_lock(&d->mutex);
			ms_bufferizer_put(d->bufferizer, m);
			if (d->sleeping) ms_cond_signal(&d->cond);
			d->last_sample_date = f->ticker->time;
			ms_mutex_unlock(&d->mutex);
		} else freemsg(m);
	}
}

static MSFilterDesc msandroid_sound_write_desc = {
    /*.id=*/MS_FILTER_PLUGIN_ID,
    /*.name=*/"MSAndSoundWrite",
    /*.text=*/N_("Sound playback filter for Android"),
    /*.category=*/MS_FILTER_OTHER,
    /*.enc_fmt*/ NULL,
    /*.ninputs=*/1,
    /*.noutputs=*/0,
    /*.init*/ NULL,
    /*.preprocess=*/msandroid_sound_write_preprocess,
    /*.process=*/msandroid_sound_write_process,
    /*.postprocess=*/msandroid_sound_write_postprocess,
    /*.uninit*/ NULL,
    /*.methods=*/msandroid_sound_write_methods};

MSFilter *msandroid_sound_write_new(MSSndCard *card) {
	ms_debug("msandroid_sound_write_new");
	MSFilter *f = ms_factory_create_filter_from_desc(ms_snd_card_get_factory(card), &msandroid_sound_write_desc);
	msandroid_sound_write_data *data = new msandroid_sound_write_data();
	if (card->data != NULL) {
		SoundDeviceDescription *d = (SoundDeviceDescription *)card->data;
		if (d->recommended_rate > 0) {
			data->rate = d->recommended_rate;
			data->forced_rate = true;
			ms_warning("Using forced sample rate %i", data->rate);
		}
	}
	f->data = data;
	return f;
}

/******* Hack for Galaxy S ***********/

static int msandroid_hack_speaker_state(MSFilter *f, void *arg) {
	msandroid_sound_read_data *d = (msandroid_sound_read_data *)f->data;
	bool speakerOn = *((bool *)arg);

	if (!d->started) {
		ms_error("Audio recorder not started, can't hack speaker");
		return -1;
	}

	JNIEnv *jni_env = ms_get_jni_env();

	// First, check that required methods are found
	jclass LinphoneManager_class = (jclass)jni_env->NewGlobalRef(jni_env->FindClass("org/linphone/LinphoneManager"));
	if (LinphoneManager_class == 0) {
		ms_error("Cannot find org/linphone/LinphoneManager");
		return -1;
	}
	jclass LinphoneCoreImpl_class =
	    (jclass)jni_env->NewGlobalRef(jni_env->FindClass("org/linphone/core/LinphoneCoreImpl"));
	if (LinphoneCoreImpl_class == 0) {
		ms_error("Cannot find org/linphone/core/LinphoneCoreImpl");
		return -1;
	}
	jmethodID getLc_id =
	    jni_env->GetStaticMethodID(LinphoneManager_class, "getLc", "()Lorg/linphone/core/LinphoneCore;");
	if (getLc_id == 0) {
		ms_error("Cannot find LinphoneManager.getLc()");
		return -1;
	}
	jmethodID routeAudioToSpeakerHelper_id =
	    jni_env->GetMethodID(LinphoneCoreImpl_class, "routeAudioToSpeakerHelper", "(Z)V");
	if (routeAudioToSpeakerHelper_id == 0) {
		ms_error("Cannot find LinphoneCoreImpl.routeAudioToSpeakerHelper()");
		return -1;
	}
	jobject lc = jni_env->CallStaticObjectMethod(LinphoneManager_class, getLc_id);

	ms_mutex_lock(&d->mutex);
	d->started = false;
	ms_mutex_unlock(&d->mutex);

	// Stop audio recorder
	ms_message("Hacking speaker state: calling sound_read_postprocess()");
	sound_read_postprocess(f);

	// Flush eventual sound in the buffer
	// No need to lock as reader_cb is stopped
	ms_bufferizer_flush(&d->rb);

	// Change speaker state by calling back to java code
	// as there seems to be no way to get a reference to AudioManager service.
	ms_message("Hacking speaker state: do magic from LinphoneCoreImpl.RouteAudioToSpeakerHelper()");
	jni_env->CallVoidMethod(lc, routeAudioToSpeakerHelper_id, speakerOn);

	// Re-open audio and set d->started=true
	ms_message("Hacking speaker state: calling sound_read_preprocess()");
	sound_read_preprocess(f);

	return 0;
}

/******* End Hack for Galaxy S ***********/
